package com.pine.common.systemlog.aspect;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import com.pine.common.beans.LogId;
import com.pine.common.beans.ResultVo;
import com.pine.common.systemlog.AopUtils;
import com.pine.common.systemlog.LogTypeEnum;
import com.pine.common.systemlog.SysLogContext;
import com.pine.common.systemlog.SysLogVo;
import com.pine.common.systemlog.annotation.SysLog;
import com.pine.common.systemlog.event.SysLogEvent;
import com.pine.common.util.user.UserInfo;
import com.pine.common.util.user.UserUtil;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Objects;

/**
 * 系统日志切面
 *
 * @author song tools
 * @since 2024-07-04
 */
@Aspect
@Slf4j
public class SystemLogAspect implements ApplicationEventPublisherAware, InitializingBean {

    private static final String FORM_DATA_CONTENT_TYPE = "multipart/form-data";

    /**
     * 最大内容长度
     */
    private static final int MAX_CONTENT_LENGTH = 65535;

    private static ApplicationEventPublisher applicationEventPublisher;


    /**
     * 定义切点，用于匹配标注了SysLog注解的方法。
     * <p>
     * Pointcut注解用于声明切点表达式，这里的切点表达式指定了所有标注了com.pine.common.systemlog.annotation.SysLog注解的方法。
     * 这样做的目的是为了在AOP切面中对这些方法进行拦截，进而执行额外的逻辑，如日志记录等。
     */
    @Pointcut("@annotation(com.pine.common.systemlog.annotation.SysLog)")
    @SuppressWarnings("unused")
    private void pointCut() {
    }


    @Before("pointCut()")
    public void doBefore(JoinPoint joinPoint) {
        tryCatch(() -> {
            SysLog sysLog = AopUtils.getTargetAnnotation(joinPoint, SysLog.class);
            if (sysLog == null || !sysLog.enable()) {
                return;
            }

            SysLogVo sysLogVo = buildSysLogVo(joinPoint, sysLog);
            SysLogContext.put(sysLogVo);
        });
    }

    @AfterReturning(value = "pointCut()", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Object result) {
        tryCatch(() -> {
            SysLog sysLog = AopUtils.getTargetAnnotation(joinPoint, SysLog.class);
            if (sysLog == null || !sysLog.enable()) {
                return;
            }

            SysLogVo sysLogVo = SysLogContext.get();
            sysLogVo.setLogType(LogTypeEnum.INFO);

            ResultVo<?> resultVo = Convert.convert(ResultVo.class, result);
            if (resultVo != null) {
                // 业务异常
                if (!resultVo.isSuccess()) {
                    sysLogVo.setLogType(LogTypeEnum.ERROR);
                    sysLogVo.setError(resultVo.getMessage());
                }
            }

            if (sysLog.response()) {
                if (result == null) {
                    sysLogVo.setResponseParam("{}");
                } else {
                    sysLogVo.setResponseParam(getText(JSONUtil.toJsonStr(result)));
                }
            }

            publishEvent(sysLogVo);
        });
    }

    @AfterThrowing(value = "pointCut()", throwing = "throwable")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable throwable) {
        tryCatch(() -> {
            SysLog sysLog = AopUtils.getTargetAnnotation(joinPoint, SysLog.class);
            if (sysLog == null || !sysLog.enable()) {
                return;
            }
            SysLogVo sysLogVo = SysLogContext.get();
            sysLogVo.setLogType(LogTypeEnum.ERROR);
            sysLogVo.setError(ExceptionUtil.stacktraceToString(throwable, MAX_CONTENT_LENGTH));
            publishEvent(sysLogVo);
        });
    }

    private void publishEvent(SysLogVo sysLog) {
        buildUserInfo(sysLog);
        sysLog.setFinishTime(LocalDateTime.now());
        sysLog.setConsumingTime(sysLog.getStartTime().until(sysLog.getFinishTime(), ChronoUnit.MILLIS));
        applicationEventPublisher.publishEvent(new SysLogEvent(sysLog));
        SysLogContext.clean();
    }

    private void buildUserInfo(SysLogVo sysLogVo) {
        UserInfo userInfo = UserUtil.get();
        if (userInfo != null) {
            sysLogVo.setUserId(String.valueOf(userInfo.id()));
            sysLogVo.setUserName(userInfo.username());
        } else {
            sysLogVo.setUserId("0");
            sysLogVo.setUserName("system");
        }
    }

    private SysLogVo buildSysLogVo(JoinPoint joinPoint, SysLog sysLog) {
        SysLogVo sysLogVo = SysLogContext.get();

        // 类名
        sysLogVo.setClassPath(joinPoint.getTarget().getClass().getName());
        // 获取执行的方法名
        sysLogVo.setActionMethod(joinPoint.getSignature().getName());
        // 日志id
        sysLogVo.setLogId(LogId.get());
        // 设置用户信息
        UserInfo userInfo = UserUtil.get();
        if (userInfo != null) {
            sysLogVo.setUserId(String.valueOf(userInfo.id()));
            sysLogVo.setUserName(userInfo.username());
        } else {
            sysLogVo.setUserId("0");
            sysLogVo.setUserName("system");
        }
        // 开始时间
        sysLogVo.setStartTime(LocalDateTime.now());
        // 请求参数
        HttpServletRequest request = setAndGetRequestParam(joinPoint, sysLog, sysLogVo);
        // 请求ip
        sysLogVo.setRequestIp(JakartaServletUtil.getClientIP(request));
        // 请求uri
        sysLogVo.setRequestUri(URLUtil.getPath(request.getRequestURI()));
        // 请求方式
        sysLogVo.setHttpMethod(request.getMethod());
        // 描述
        setDescription(joinPoint, sysLog, sysLogVo);
        return sysLogVo;
    }

    private HttpServletRequest setAndGetRequestParam(JoinPoint joinPoint, SysLog sysLog, SysLogVo sysLogVo) {
        // 参数
        Object[] args = joinPoint.getArgs();

        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes(), "只能在Spring Web环境使用@SysLog记录日志")).getRequest();
        if (sysLog.request()) {
            String strArgs = getArgs(args, request);
            sysLogVo.setRequestParam(getText(strArgs));
        }
        return request;
    }

    private String getArgs(Object[] args, HttpServletRequest request) {
        String strArgs = "{}";
        Object[] params = Arrays.stream(args).filter(item -> !(item instanceof ServletRequest || item instanceof ServletResponse || item instanceof MultipartFile || item instanceof File)).toArray();

        try {
            if (!request.getContentType().contains(FORM_DATA_CONTENT_TYPE)) {
                if (ObjectUtil.isNotEmpty(params)) {
                    if (params.length == 1) {
                        strArgs = JSONUtil.toJsonStr(params[0]);
                    } else {
                        strArgs = JSONUtil.toJsonStr(params);
                    }
                }
            }
        } catch (Exception e) {
            try {
                strArgs = Arrays.toString(params);
            } catch (Exception ex) {
                log.warn("解析参数异常", ex);
            }
        }
        return strArgs;
    }

    private void setDescription(JoinPoint joinPoint, SysLog sysLog, SysLogVo sysLogVo) {
        String controllerDescription = "";
        Tag api = joinPoint.getTarget().getClass().getAnnotation(Tag.class);
        if (api != null) {
            controllerDescription = api.name();
        }
        String controllerMethodDescription = sysLog.value();
        if (ObjectUtil.isEmpty(controllerDescription)) {
            sysLogVo.setDescription(controllerMethodDescription);
        } else {
            sysLogVo.setDescription(controllerDescription + ":" + controllerMethodDescription);
        }
    }

    /**
     * 截取指定长度的字符串
     *
     * @param val 参数
     * @return 截取文本
     */
    private String getText(String val) {
        return StrUtil.sub(val, 0, MAX_CONTENT_LENGTH);
    }


    private void tryCatch(Runnable runnable) {
        try {
            runnable.run();
        } catch (Exception e) {
            log.warn("系统日志切面异常", e);
            SysLogContext.clean();
        }
    }


    @Override
    public void setApplicationEventPublisher(@NonNull ApplicationEventPublisher applicationEventPublisher) {
        SystemLogAspect.applicationEventPublisher = applicationEventPublisher;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("初始化系统日志切面");
    }
}
